<?php

class XMLParser {
	var $parser;

	/**
	* The XML document
	*
	* @var string
	*/
	var $xml;

	/**
	* Document tag
	*
	* @var object
	*/
	var $document;

	/**
	* Current object depth
	*
	* @var array
	*/
	var $stack;

	/**
	 * Constructor. Loads XML document.
	 *
	 * @param string $xml The string of the XML document
	 * @return XMLParser
	 */
	function XMLParser($xml = '') {
		//Load XML document
		$this->xml = $xml;

		// Set stack to an array
		$this->stack = array ();
	}

	/**
	 * Initiates and runs PHP's XML parser
	 */
	function Parse() {
		//Create the parser resource
		$this->parser = xml_parser_create();

		//Set the handlers
		xml_set_object($this->parser, $this);
		xml_set_element_handler($this->parser, 'StartElement', 'EndElement');
		xml_set_character_data_handler($this->parser, 'CharacterData');

		//Error handling
		if (!xml_parse($this->parser, $this->xml))
			$this->HandleError(xml_get_error_code($this->parser), xml_get_current_line_number($this->parser), xml_get_current_column_number($this->parser));

		//Free the parser
		xml_parser_free($this->parser);
	}

	/**
	 * Handles an XML parsing error
	 *
	 * @param int $code XML Error Code
	 * @param int $line Line on which the error happened
	 * @param int $col Column on which the error happened
	 */
	function HandleError($code, $line, $col) {
		trigger_error('XML Parsing Error at ' . $line . ':' . $col . '. Error ' . $code . ': ' . xml_error_string($code));
	}

	/**
	 * Gets the XML output of the PHP structure within $this->document
	 *
	 * @return string
	 */
	function GenerateXML() {
		return $this->document->GetXML();
	}

	/**
	 * Gets the reference to the current direct parent
	 *
	 * @return object
	 */
	function GetStackLocation() {
		$return = '';

		foreach ($this->stack as $stack)
			$return .= $stack . '->';

		return rtrim($return, '->');
	}

	/**
	 * Handler function for the start of a tag
	 *
	 * @param resource $parser
	 * @param string $name
	 * @param array $attrs
	 */
	function StartElement($parser, $name, $attrs = array ()) {
		//Make the name of the tag lower case
		$name = strtolower($name);

		//Check to see if tag is root-level
		if (count($this->stack) == 0) {
			//If so, set the document as the current tag
			$this->document = new XMLTag($name, $attrs);

			//And start out the stack with the document tag
			$this->stack = array (
				'document'
			);
		}
		//If it isn't root level, use the stack to find the parent
		else {
			//Get the name which points to the current direct parent, relative to $this
			$parent = $this->GetStackLocation();

			//Add the child
			eval ('$this->' . $parent . '->AddChild($name, $attrs, ' . count($this->stack) . ');');

			//Update the stack
			eval ('$this->stack[] = $name.\'[\'.(count($this->' . $parent . '->' . $name . ') - 1).\']\';');
		}
	}

	/**
	 * Handler function for the end of a tag
	 *
	 * @param resource $parser
	 * @param string $name
	 */
	function EndElement($parser, $name) {
		//Update stack by removing the end value from it as the parent
		array_pop($this->stack);
	}

	/**
	 * Handler function for the character data within a tag
	 *
	 * @param resource $parser
	 * @param string $data
	 */
	function CharacterData($parser, $data) {
		//Get the reference to the current parent object
		$tag = $this->GetStackLocation();

		//Assign data to it
		eval ('$this->' . $tag . '->tagData .= trim($data);');
	}
}

/**
* XML Tag Object (php4)
*
* This object stores all of the direct children of itself in the $children array. They are also stored by
* type as arrays. So, if, for example, this tag had 2 <font> tags as children, there would be a class member
* called $font created as an array. $font[0] would be the first font tag, and $font[1] would be the second.
*
* To loop through all of the direct children of this object, the $children member should be used.
*
* To loop through all of the direct children of a specific tag for this object, it is probably easier
* to use the arrays of the specific tag names, as explained above.
*
* @author Adam A. Flynn <adamaflynn@thousandmonkeys.net>
* @copyright Copyright (c) 2005, Adam A. Flynn
*
* @version 1.2.0
*/
class XMLTag {
	/**
	 * Array with the attributes of this XML tag
	 *
	 * @var array
	 */
	var $tagAttrs;

	/**
	 * The name of the tag
	 *
	 * @var string
	 */
	var $tagName;

	/**
	 * The data the tag contains
	 *
	 * So, if the tag doesn't contain child tags, and just contains a string, it would go here
	 *
	 * @var string
	 */
	var $tagData;

	/**
	 * Array of references to the objects of all direct children of this XML object
	 *
	 * @var array
	 */
	var $tagChildren;

	/**
	 * The number of parents this XML object has (number of levels from this tag to the root tag)
	 *
	 * Used presently only to set the number of tabs when outputting XML
	 *
	 * @var int
	 */
	var $tagParents;

	/**
	 * Constructor, sets up all the default values
	 *
	 * @param string $name
	 * @param array $attrs
	 * @param int $parents
	 * @return XMLTag
	 */
	function XMLTag($name, $attrs = array (), $parents = 0) {
		//Make the keys of the attr array lower case, and store the value
		$this->tagAttrs = array_change_key_case($attrs, CASE_LOWER);

		//Make the name lower case and store the value
		$this->tagName = strtolower($name);

		//Set the number of parents
		$this->tagParents = $parents;

		//Set the types for children and data
		$this->tagChildren = array ();
		$this->tagData = '';
	}

	/**
	 * Adds a direct child to this object
	 *
	 * @param string $name
	 * @param array $attrs
	 * @param int $parents
	 */
	function AddChild($name, $attrs, $parents) {
		//If there is no array already set for the tag name being added,
		//create an empty array for it
		if (!isset ($this-> $name))
			$this-> $name = array ();

		//If the tag has the same name as a member in XMLTag, or somehow the
		//array wasn't properly created, output a more informative error than
		//PHP otherwise would.
		if (!is_array($this-> $name)) {
			trigger_error('You have used a reserved name as the name of an XML tag. Please consult the documentation (http://www.thousandmonkeys.net/xml_doc.php) and rename the tag named ' . $name . ' to something other than a reserved name.', E_USER_ERROR);

			return;
		}

		//Create the child object itself
		$child = new XMLTag($name, $attrs, $parents);

		//Add the reference of it to the end of an array member named for the tag's name
		$this-> {
			$name }
		[] = & $child;

		//Add the reference to the children array member
		$this->tagChildren[] = & $child;
	}

	/**
	 * Returns the string of the XML document which would be generated from this object
	 *
	 * This function works recursively, so it gets the XML of itself and all of its children, which
	 * in turn gets the XML of all their children, which in turn gets the XML of all thier children,
	 * and so on. So, if you call GetXML from the document root object, it will return a string for
	 * the XML of the entire document.
	 *
	 * This function does not, however, return a DTD or an XML version/encoding tag. That should be
	 * handled by XMLParser::GetXML()
	 *
	 * @return string
	 */
	function GetXML() {
		//Start a new line, indent by the number indicated in $this->parents, add a <, and add the name of the tag
		$out = "\n" . str_repeat("\t", $this->tagParents) . '<' . $this->tagName;

		//For each attribute, add attr="value"
		foreach ($this->tagAttrs as $attr => $value)
			$out .= ' ' . $attr . '="' . $value . '"';

		//If there are no children and it contains no data, end it off with a />
		if (empty ($this->tagChildren) && empty ($this->tagData))
			$out .= " />";

		//Otherwise...
		else {
			//If there are children
			if (!empty ($this->tagChildren)) {
				//Close off the start tag
				$out .= '>';

				//For each child, call the GetXML function (this will ensure that all children are added recursively)
				foreach ($this->tagChildren as $child)
					$out .= $child->GetXML();

				//Add the newline and indentation to go along with the close tag
				$out .= "\n" . str_repeat("\t", $this->tagParents);
			}

			//If there is data, close off the start tag and add the data
			elseif (!empty ($this->tagData)) $out .= '>' . $this->tagData;

			//Add the end tag    
			$out .= '</' . $this->tagName . '>';
		}

		//Return the final output
		return $out;
	}
}
?>